using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

#pragma warning disable 1591

// ReSharper disable once CheckNamespace
namespace OpenGauss.NET.Types
{
    [Obsolete(
        "For values outside the range of DateTime/DateOnly, consider using NodaTime (range -9998 to 9999), or read the value as an 'int'. " +
        "See https://www.opengauss.org/doc/types/datetime.html for more information.")]
    [Serializable]
    public readonly struct OpenGaussDate : IEquatable<OpenGaussDate>, IComparable<OpenGaussDate>, IComparable,
        IComparer<OpenGaussDate>, IComparer
    {
        //Number of days since January 1st CE (January 1st EV). 1 Jan 1 CE = 0, 2 Jan 1 CE = 1, 31 Dec 1 BCE = -1, etc.
        readonly int _daysSinceEra;
        readonly InternalType _type;

        #region Constants

        static readonly int[] CommonYearDays = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
        static readonly int[] LeapYearDays = { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 };
        static readonly int[] CommonYearMaxes = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
        static readonly int[] LeapYearMaxes = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

        /// <summary>
        /// Represents the date 1970-01-01
        /// </summary>
        public static readonly OpenGaussDate Epoch = new(1970, 1, 1);

        /// <summary>
        /// Represents the date 0001-01-01
        /// </summary>
        public static readonly OpenGaussDate Era = new(0);

        public const int MaxYear = 5874897;
        public const int MinYear = -4714;
        public static readonly OpenGaussDate MaxCalculableValue = new(MaxYear, 12, 31);
        public static readonly OpenGaussDate MinCalculableValue = new(MinYear, 11, 24);

        public static readonly OpenGaussDate Infinity = new(InternalType.Infinity);
        public static readonly OpenGaussDate NegativeInfinity = new(InternalType.NegativeInfinity);

        const int DaysInYear = 365; //Common years
        const int DaysIn4Years = 4 * DaysInYear + 1; //Leap year every 4 years.
        const int DaysInCentury = 25 * DaysIn4Years - 1; //Except no leap year every 100.
        const int DaysIn4Centuries = 4 * DaysInCentury + 1; //Except leap year every 400.

        #endregion

        #region Constructors

        OpenGaussDate(InternalType type)
        {
            _type = type;
            _daysSinceEra = 0;
        }

        internal OpenGaussDate(int days)
        {
            _type = InternalType.Finite;
            _daysSinceEra = days;
        }

        public OpenGaussDate(DateTime dateTime) : this((int)(dateTime.Ticks / TimeSpan.TicksPerDay)) {}

        public OpenGaussDate(OpenGaussDate copyFrom) : this(copyFrom._daysSinceEra) {}

        public OpenGaussDate(int year, int month, int day)
        {
            _type = InternalType.Finite;
            if (year == 0 || year < MinYear || year > MaxYear || month < 1 || month > 12 || day < 1 ||
                (day > (IsLeap(year) ? 366 : 365)))
            {
                throw new ArgumentOutOfRangeException();
            }

            _daysSinceEra = DaysForYears(year) + (IsLeap(year) ? LeapYearDays : CommonYearDays)[month - 1] + day - 1;
        }

        #endregion

        #region String Conversions

        public override string ToString()
            => _type switch
            {
                InternalType.Infinity         => "infinity",
                InternalType.NegativeInfinity => "-infinity",
                //Format of yyyy-MM-dd with " BC" for BCE and optional " AD" for CE which we omit here.
                _ => new StringBuilder(Math.Abs(Year).ToString("D4"))
                    .Append('-').Append(Month.ToString("D2"))
                    .Append('-').Append(Day.ToString("D2"))
                    .Append(_daysSinceEra < 0 ? " BC" : "").ToString()
            };

        public static OpenGaussDate Parse(string str)
        {

            if (str == null) {
                throw new ArgumentNullException(nameof(str));
            }

            if (str == "infinity")
                return Infinity;

            if (str == "-infinity")
                return NegativeInfinity;

            str = str.Trim();
            try {
                var idx = str.IndexOf('-');
                if (idx == -1) {
                    throw new FormatException();
                }
                var year = int.Parse(str.Substring(0, idx));
                var idxLast = idx + 1;
                if ((idx = str.IndexOf('-', idxLast)) == -1) {
                    throw new FormatException();
                }
                var month = int.Parse(str.Substring(idxLast, idx - idxLast));
                idxLast = idx + 1;
                if ((idx = str.IndexOf(' ', idxLast)) == -1) {
                    idx = str.Length;
                }
                var day = int.Parse(str.Substring(idxLast, idx - idxLast));
                if (str.Contains("BC")) {
                    year = -year;
                }
                return new OpenGaussDate(year, month, day);
            } catch (OverflowException) {
                throw;
            } catch (Exception) {
                throw new FormatException();
            }
        }

        public static bool TryParse(string str, out OpenGaussDate date)
        {
            try {
                date = Parse(str);
                return true;
            } catch {
                date = Era;
                return false;
            }
        }

        #endregion

        #region Public Properties

        public static OpenGaussDate Now => new(DateTime.Now);
        public static OpenGaussDate Today => Now;
        public static OpenGaussDate Yesterday => Now.AddDays(-1);
        public static OpenGaussDate Tomorrow => Now.AddDays(1);

        public int DayOfYear => _daysSinceEra - DaysForYears(Year) + 1;

        public int Year
        {
            get
            {
                var guess = (int)Math.Round(_daysSinceEra/365.2425);
                var test = guess - 1;
                while (DaysForYears(++test) <= _daysSinceEra) {}
                return test - 1;
            }
        }

        public int Month
        {
            get
            {
                var i = 1;
                var target = DayOfYear;
                var array = IsLeapYear ? LeapYearDays : CommonYearDays;
                while (target > array[i])
                {
                    ++i;
                }
                return i;
            }
        }

        public int Day => DayOfYear - (IsLeapYear ? LeapYearDays : CommonYearDays)[Month - 1];

        public DayOfWeek DayOfWeek => (DayOfWeek) ((_daysSinceEra + 1)%7);

        internal int DaysSinceEra => _daysSinceEra;

        public bool IsLeapYear => IsLeap(Year);

        public bool IsInfinity => _type == InternalType.Infinity;
        public bool IsNegativeInfinity => _type == InternalType.NegativeInfinity;

        public bool IsFinite
            => _type switch {
                InternalType.Finite           => true,
                InternalType.Infinity         => false,
                InternalType.NegativeInfinity => false,
                _ => throw new InvalidOperationException($"Internal OpenGauss bug: unexpected value {_type} of enum {nameof(OpenGaussDate)}.{nameof(InternalType)}. Please file a bug.")
            };

        #endregion

        #region Internals

        static int DaysForYears(int years)
        {
            //Number of years after 1CE (0 for 1CE, -1 for 1BCE, 1 for 2CE).
            var calcYear = years < 1 ? years : years - 1;

            return calcYear / 400 * DaysIn4Centuries //Blocks of 400 years with their leap and common years
                   + calcYear % 400 / 100 * DaysInCentury //Remaining blocks of 100 years with their leap and common years
                   + calcYear % 100 / 4 * DaysIn4Years //Remaining blocks of 4 years with their leap and common years
                   + calcYear % 4 * DaysInYear //Remaining years, all common
                   + (calcYear < 0 ? -1 : 0); //And 1BCE is leap.
        }

        static bool IsLeap(int year)
        {
            //Every 4 years is a leap year
            //Except every 100 years isn't a leap year.
            //Except every 400 years is.
            if (year < 1)
            {
                year = year + 1;
            }
            return (year%4 == 0) && ((year%100 != 0) || (year%400 == 0));
        }

        #endregion

        #region Arithmetic

        public OpenGaussDate AddDays(int days)
            => _type switch
        {
            InternalType.Infinity         => Infinity,
            InternalType.NegativeInfinity => NegativeInfinity,
            InternalType.Finite           => new OpenGaussDate(_daysSinceEra + days),
            _ => throw new InvalidOperationException($"Internal OpenGauss bug: unexpected value {_type} of enum {nameof(OpenGaussDate)}.{nameof(InternalType)}. Please file a bug.")
        };

        public OpenGaussDate AddYears(int years)
        {
            switch (_type) {
            case InternalType.Infinity:
                return Infinity;
            case InternalType.NegativeInfinity:
                return NegativeInfinity;
            case InternalType.Finite:
                break;
            default:
                throw new InvalidOperationException($"Internal OpenGauss bug: unexpected value {_type} of enum {nameof(OpenGaussDate)}.{nameof(InternalType)}. Please file a bug.");
            }

            var newYear = Year + years;
            if (newYear >= 0 && _daysSinceEra < 0) //cross 1CE/1BCE divide going up
            {
                ++newYear;
            }
            else if (newYear <= 0 && _daysSinceEra >= 0) //cross 1CE/1BCE divide going down
            {
                --newYear;
            }
            return new OpenGaussDate(newYear, Month, Day);
        }

        public OpenGaussDate AddMonths(int months)
        {
            switch (_type) {
            case InternalType.Infinity:
                return Infinity;
            case InternalType.NegativeInfinity:
                return NegativeInfinity;
            case InternalType.Finite:
                break;
            default:
                throw new InvalidOperationException($"Internal OpenGauss bug: unexpected value {_type} of enum {nameof(OpenGaussDate)}.{nameof(InternalType)}. Please file a bug.");
            }

            var newYear = Year;
            var newMonth = Month + months;

            while (newMonth > 12)
            {
                newMonth -= 12;
                newYear += 1;
            }
            while (newMonth < 1)
            {
                newMonth += 12;
                newYear -= 1;
            }
            var maxDay = (IsLeap(newYear) ? LeapYearMaxes : CommonYearMaxes)[newMonth - 1];
            var newDay = Day > maxDay ? maxDay : Day;
            return new OpenGaussDate(newYear, newMonth, newDay);

        }

        public OpenGaussDate Add(in OpenGaussTimeSpan interval)
        {
            switch (_type) {
            case InternalType.Infinity:
                return Infinity;
            case InternalType.NegativeInfinity:
                return NegativeInfinity;
            case InternalType.Finite:
                break;
            default:
                throw new InvalidOperationException($"Internal OpenGauss bug: unexpected value {_type} of enum {nameof(OpenGaussDate)}.{nameof(InternalType)}. Please file a bug.");
            }

            return AddMonths(interval.Months).AddDays(interval.Days);
        }

        internal OpenGaussDate Add(in OpenGaussTimeSpan interval, int carriedOverflow)
        {
            switch (_type) {
            case InternalType.Infinity:
                return Infinity;
            case InternalType.NegativeInfinity:
                return NegativeInfinity;
            case InternalType.Finite:
                break;
            default:
                throw new InvalidOperationException($"Internal OpenGauss bug: unexpected value {_type} of enum {nameof(OpenGaussDate)}.{nameof(InternalType)}. Please file a bug.");
            }

            return AddMonths(interval.Months).AddDays(interval.Days + carriedOverflow);
        }

        #endregion

        #region Comparison

        public int Compare(OpenGaussDate x, OpenGaussDate y) => x.CompareTo(y);

        public int Compare(object? x, object? y)
        {
            if (x == null)
            {
                return y == null ? 0 : -1;
            }
            if (y == null)
            {
                return 1;
            }
            if (!(x is IComparable) || !(y is IComparable))
            {
                throw new ArgumentException();
            }
            return ((IComparable) x).CompareTo(y);
        }

        public bool Equals(OpenGaussDate other)
            => _type switch
            {
                InternalType.Infinity         => other._type == InternalType.Infinity,
                InternalType.NegativeInfinity => other._type == InternalType.NegativeInfinity,
                InternalType.Finite           => other._type == InternalType.Finite && _daysSinceEra == other._daysSinceEra,
                _ => false
            };

        public override bool Equals(object? obj) => obj is OpenGaussDate date && Equals(date);

        public int CompareTo(OpenGaussDate other)
            => _type switch
            {
                InternalType.Infinity         => other._type == InternalType.Infinity ? 0 : 1,
                InternalType.NegativeInfinity => other._type == InternalType.NegativeInfinity ? 0 : -1,
                _ => other._type switch
                {
                    InternalType.Infinity         => -1,
                    InternalType.NegativeInfinity => 1,
                    _                             => _daysSinceEra.CompareTo(other._daysSinceEra)
                }
            };

        public int CompareTo(object? o)
            => o == null
                ? 1
                : o is OpenGaussDate opengaussDate
                    ? CompareTo(opengaussDate)
                    : throw new ArgumentException();

        public override int GetHashCode() => _daysSinceEra;

        #endregion

        #region Operators

        public static bool operator ==(OpenGaussDate x, OpenGaussDate y) => x.Equals(y);
        public static bool operator !=(OpenGaussDate x, OpenGaussDate y) => !(x == y);
        public static bool operator <(OpenGaussDate x, OpenGaussDate y)  => x.CompareTo(y) < 0;
        public static bool operator >(OpenGaussDate x, OpenGaussDate y)  => x.CompareTo(y) > 0;
        public static bool operator <=(OpenGaussDate x, OpenGaussDate y) => x.CompareTo(y) <= 0;
        public static bool operator >=(OpenGaussDate x, OpenGaussDate y) => x.CompareTo(y) >= 0;

        public static DateTime ToDateTime(OpenGaussDate date)
        {
            switch (date._type)
            {
            case InternalType.Infinity:
            case InternalType.NegativeInfinity:
                throw new InvalidCastException("Infinity values can't be cast to DateTime");
            case InternalType.Finite:
                try { return new DateTime(date._daysSinceEra * OpenGaussTimeSpan.TicksPerDay); }
                catch { throw new InvalidCastException(); }
            default:
                throw new InvalidOperationException($"Internal OpenGauss bug: unexpected value {date._type} of enum {nameof(OpenGaussDate)}.{nameof(InternalType)}. Please file a bug.");
            }
        }

        public static explicit operator DateTime(OpenGaussDate date) => ToDateTime(date);

        public static OpenGaussDate ToOpenGaussDate(DateTime date)
            => new((int)(date.Ticks / OpenGaussTimeSpan.TicksPerDay));

        public static explicit operator OpenGaussDate(DateTime date) => ToOpenGaussDate(date);

        public static OpenGaussDate operator +(OpenGaussDate date, OpenGaussTimeSpan interval)
            => date.Add(interval);

        public static OpenGaussDate operator +(OpenGaussTimeSpan interval, OpenGaussDate date)
            => date.Add(interval);

        public static OpenGaussDate operator -(OpenGaussDate date, OpenGaussTimeSpan interval)
            => date.Subtract(interval);

        public OpenGaussDate Subtract(in OpenGaussTimeSpan interval) => Add(-interval);

        public static OpenGaussTimeSpan operator -(OpenGaussDate dateX, OpenGaussDate dateY)
        {
            if (dateX._type != InternalType.Finite || dateY._type != InternalType.Finite)
                throw new ArgumentException("Can't subtract infinity date values");

            return new OpenGaussTimeSpan(0, dateX._daysSinceEra - dateY._daysSinceEra, 0);
        }

        #endregion

#if NET6_0_OR_GREATER
        public OpenGaussDate(DateOnly date) : this(date.Year, date.Month, date.Day) {}

        public static DateOnly ToDateOnly(OpenGaussDate date)
        {
            switch (date._type)
            {
            case InternalType.Infinity:
            case InternalType.NegativeInfinity:
                throw new InvalidCastException("Infinity values can't be cast to DateTime");
            case InternalType.Finite:
                try { return new DateOnly(date.Year, date.Month, date.Day); }
                catch { throw new InvalidCastException(); }
            default:
                throw new InvalidOperationException($"Internal OpenGauss bug: unexpected value {date._type} of enum {nameof(OpenGaussDate)}.{nameof(InternalType)}. Please file a bug.");
            }
        }

        public static explicit operator DateOnly(OpenGaussDate date) => ToDateOnly(date);

        public static OpenGaussDate ToOpenGaussDate(DateOnly date)
            => new(date.Year, date.Month, date.Day);

        public static explicit operator OpenGaussDate(DateOnly date) => ToOpenGaussDate(date);
#endif

        enum InternalType
        {
            Finite,
            Infinity,
            NegativeInfinity
        }
    }
}
